{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4ba51a29-a566-4b5a-97f0-10e634567e40",
   "metadata": {
    "collapsed": true,
    "jupyter": {
     "outputs_hidden": true
    }
   },
   "source": [
    "# Transform OpenAPI apis into MCP tools using Bedrock AgentCore Gateway\n",
    "\n",
    "## Overview\n",
    "Bedrock AgentCore Gateway provides customers a way to turn their existing APIs into fully-managed MCP servers without needing to manage infra or hosting. Customers can bring OpenAPI spec in JSON or YAML. We will demonstrate a customer service agent using enterprise support apis secured by OAuth2.\n",
    "\n",
    "The Gateway workflow involves the following steps to connect your agents to external tools:\n",
    "* **Create the tools for your Gateway** - Define your tools using schemas such as OpenAPI specifications for REST APIs. The OpenAPI specifications are then parsed by Amazon Bedrock AgentCore for creating the Gateway.\n",
    "* **Create a Gateway endpoint** - Create the gateway that will serve as the MCP entry point with inbound authentication.\n",
    "* **Add targets to your Gateway** - Configure the OpenAPI targets that define how the gateway routes requests to specific tools. All the APIs that part of OpenAPI file will become an MCP-compatible tool, and will be made available through your Gateway endpoint URL. Configure outbound authorization using Oauth for each OpenAPI Gateway target. \n",
    "* **Update your agent code** - Connect your agent to the Gateway endpoint to access all configured tools through the unified MCP interface.\n",
    "\n",
    "![How does it work](images/openapis-oauth-gateway.png)\n",
    "\n",
    "### Tutorial Details\n",
    "\n",
    "\n",
    "| Information          | Details                                                   |\n",
    "|:---------------------|:----------------------------------------------------------|\n",
    "| Tutorial type        | Interactive                                               |\n",
    "| AgentCore components | AgentCore Gateway, AgentCore Identity                     |\n",
    "| Agentic Framework    | Strands Agent                                             |\n",
    "| Gateway Target type  | OpenAPI                                                   |\n",
    "| Agent                | Customer support agent                                    |\n",
    "| Inbound Auth IdP     | Okta                                                      |\n",
    "| Outbound Auth        | OAuth                                                     |\n",
    "| LLM model            | Anthropic Claude Sonnet 3.7, Amazon Nova Pro              |\n",
    "| Tutorial components  | Creating AgentCore Gateway and Invoking AgentCore Gateway |\n",
    "| Tutorial vertical    | Cross-vertical                                            |\n",
    "| Example complexity   | Easy                                                      |\n",
    "| SDK used             | boto3                                                     |\n",
    "\n",
    "In the first part of the tutorial we will create some AmazonCore Gateway targets\n",
    "\n",
    "### Tutorial Architecture\n",
    "In this tutorial we will transform operations defined in OpenAPI yaml/json file into MCP tools and host it in Bedrock AgentCore Gateway.\n",
    "For demonstration purposes, we will build a Customer support agent that answers queries related to support tickets. The agent uses OpenAPIs of Zendesk support apis. The solution uses Langchain Agent using Amazon Bedrock models"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5362e1ad-f027-4452-a8d9-0b861c0115c2",
   "metadata": {},
   "source": [
    "## Prerequisites\n",
    "\n",
    "To execute this tutorial you will need:\n",
    "* Jupyter notebook with Python 3.10+\n",
    "* uv\n",
    "* AWS credentials\n",
    "* Okta\n",
    "    - client_id\n",
    "    - client_secret\n",
    "    - Your Okta domain (e.g., dev-123456.okta.com)\n",
    "    - An OAuth2 authorization server ID (often default)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a17e1d70-9a76-42ce-b1ac-1c3a0bf4d12c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Make sure you download the latest botocore and boto3 libraries.\n",
    "import shutil\n",
    "import subprocess\n",
    "import sys\n",
    "\n",
    "def ensure_uv_installed():\n",
    "    if shutil.which(\"uv\") is None:\n",
    "        print(\"🔧 'uv' not found. Installing with pip...\")\n",
    "        subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"uv\"])\n",
    "    else:\n",
    "        print(\"✅ 'uv' is already installed.\")\n",
    "\n",
    "def uv_install(*packages):\n",
    "    ensure_uv_installed()\n",
    "    uv_path = shutil.which(\"uv\")\n",
    "    print(f\"📦 Installing {', '.join(packages)} using uv...\")\n",
    "    subprocess.check_call([uv_path, \"pip\", \"install\", *packages])\n",
    "\n",
    "uv_install(\"botocore\", \"boto3\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4e6248d-b740-418b-ae0d-c0a9623e43e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set AWS credentials if not using SageMaker notebooks\n",
    "import os\n",
    "os.environ['AWS_ACCESS_KEY_ID'] = ''\n",
    "os.environ['AWS_SECRET_ACCESS_KEY'] = ''\n",
    "os.environ['AWS_DEFAULT_REGION'] = 'us-east-1'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64829bf6-fba1-4d02-8bbf-451fc5deecc8",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    "\n",
    "# Get the directory of the current script\n",
    "if '__file__' in globals():\n",
    "    current_dir = os.path.dirname(os.path.abspath(__file__))\n",
    "else:\n",
    "    current_dir = os.getcwd()  # Fallback if __file__ is not defined (e.g., Jupyter)\n",
    "\n",
    "# Navigate to the directory containing utils.py (one level up)\n",
    "utils_dir = os.path.abspath(os.path.join(current_dir, '../..'))\n",
    "\n",
    "# Add to sys.path\n",
    "sys.path.insert(0, utils_dir)\n",
    "\n",
    "# Now you can import utils\n",
    "import utils"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "10518aa1-3ac3-4610-a0cc-39c9c2eafe2c",
   "metadata": {},
   "outputs": [],
   "source": [
    "#### Create an IAM role for the Gateway to assume\n",
    "import utils\n",
    "\n",
    "agentcore_gateway_iam_role = utils.create_agentcore_gateway_role(\"sample-lambdagateway\")\n",
    "print(\"Agentcore gateway role ARN: \", agentcore_gateway_iam_role['Role']['Arn'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65120594-c3ec-4d51-810b-8d478851d8d2",
   "metadata": {},
   "source": [
    "# Configuring Okta for Inbound authorization to Gateway\n",
    "\n",
    "Below are the steps for creating Okta OAuth authorizer -\n",
    "\n",
    "* Follow instructions [here](https://developer.okta.com/docs/guides/implement-grant-type/clientcreds/main/) and create Application with client credentials grant type. If you have a Okta subscription, you can log-in to your Admin console and follow the steps outlined [here](https://developer.okta.com/docs/guides/implement-grant-type/clientcreds/main/). If you do not have Okta subscription, you will need to sign-up for a free trial.\n",
    "* Go to Admin -> Applications -> Create a client with secret. Disable Require Demonstrating Proof of Possession (DPoP) header in token requests\n",
    "* Go to Okta Admin -> Security -> API. Use the default Authorization Server and modify with additional scopes (i.e. InvokeGateway). You can optionally add Access policies and claims\n",
    "* Define a Custom Scope. Click on your authorization server name, Go to the Scopes tab, Click “Add Scope”\n",
    "* Once configured, you will need the Metadata URI for your default Authorization Server (a.k.a Discovery URI) and ClientID/Secret to configure the custom JWT Authorizer for creating the Gateway as shown below"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f1a63450-7fb9-42fc-ab4f-3d86c27bb2f8",
   "metadata": {},
   "source": [
    "# Create the Gateway with Okta authorizer for inbound authorization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "72f2cd57-7777-42d3-b6f3-c45ed0a935c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import boto3\n",
    "from pprint import pprint\n",
    "gateway_client = boto3.client('bedrock-agentcore-control', region_name = os.environ['AWS_DEFAULT_REGION'])\n",
    "\n",
    "OKTA_DISCOVERY_URL=\"https://<YOUR OKTA DOMAIN>/oauth2/<Your app>/.well-known/openid-configuration\"\n",
    "OKTA_AUDIENCE=\"<Your audience>\" # e.g. MCPGateway. It should match with your configuration in Okta\n",
    "\n",
    "auth_config = {\n",
    "        \"customJWTAuthorizer\": {\n",
    "            \"allowedAudience\": [OKTA_AUDIENCE],\n",
    "            \"discoveryUrl\": OKTA_DISCOVERY_URL\n",
    "        }\n",
    "}\n",
    "create_response = gateway_client.create_gateway(name='OpenAPIOktaGwy2',\n",
    "    roleArn = agentcore_gateway_iam_role['Role']['Arn'], # The IAM Role must have permissions to create/list/get/delete Gateway \n",
    "    protocolType='MCP',\n",
    "    authorizerType='CUSTOM_JWT',\n",
    "    authorizerConfiguration=auth_config, \n",
    "    description='AgentCore Gateway created from sdk with Okta Authorizer'\n",
    ")\n",
    "pprint(create_response)\n",
    "# Retrieve the GatewayID used for GatewayTarget creation\n",
    "gatewayID = create_response[\"gatewayId\"]\n",
    "gatewayURL = create_response[\"gatewayUrl\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1345c920-3921-40c7-9c00-110e8d02184b",
   "metadata": {},
   "source": [
    "# Transforming Zendesk support APIs into MCP tools using Bedrock AgentCore Gateway"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ea26177-4637-4b1f-82bd-6d65bfe8dc85",
   "metadata": {},
   "source": [
    "### Create outbound auth credentials provider"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64caa620-02ec-424a-a8b9-eb00b39289bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "from botocore.config import Config\n",
    "ZENDESK_DOMAIN=\"<Zendek domain url>\"\n",
    "ZENDESK_AUTH_ENDPOINT=\"https://<Zendeskl-domain>/oauth/authorizations/new\"\n",
    "ZENDESK_TOKEN_ENDPOINT=\"https://<Zendesk-domain>/oauth/tokens\"\n",
    "ZENDESK_CLIENT_ID=\"\" # Your Zendesk OAuth client -  client id \n",
    "ZENDESK_SECRET=\"\"  # Your Zendesk OAuth client -  client id \n",
    "\n",
    "sdk_config = Config(\n",
    "    region_name=os.environ['AWS_DEFAULT_REGION'],\n",
    "    retries={\"max_attempts\": 2, \"mode\": \"standard\"},\n",
    ")\n",
    "\n",
    "acps = boto3.client(\n",
    "    service_name=\"bedrock-agentcore-control\",\n",
    "    config=sdk_config,\n",
    ")\n",
    "\n",
    "provider_config= {\n",
    "    \"customOauth2ProviderConfig\": {\n",
    "         \"oauthDiscovery\": {\n",
    "             \"authorizationServerMetadata\": {\n",
    "                 \"issuer\": ZENDESK_DOMAIN,\n",
    "                 \"authorizationEndpoint\": ZENDESK_AUTH_ENDPOINT,\n",
    "                 \"tokenEndpoint\": ZENDESK_TOKEN_ENDPOINT,\n",
    "                 \"responseTypes\": [\"token\"]\n",
    "             }\n",
    "         },\n",
    "         \"clientId\": ZENDESK_CLIENT_ID,\n",
    "         \"clientSecret\": ZENDESK_SECRET\n",
    "     }\n",
    " }\n",
    "\n",
    "response = acps.create_oauth2_credential_provider(\n",
    "    name=\"ZendeskOAuthTokenCfg\", \n",
    "    credentialProviderVendor=\"CustomOauth2\", \n",
    "    oauth2ProviderConfigInput=provider_config\n",
    ")\n",
    "\n",
    "pprint(response)\n",
    "credentialProviderARN = response['credentialProviderArn']\n",
    "pprint(f\"Egress Credentials provider ARN, {credentialProviderARN}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "510b092f-be1d-4b31-abb7-1fb8cab887ef",
   "metadata": {},
   "source": [
    "### Create an OpenAPI target "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c1cb1835-5aaf-471d-ac28-6f5fe78caf5a",
   "metadata": {},
   "source": [
    "#### Upload the Zendesk support OpenAPI yaml file in S3"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a105be4e-ae05-4981-a609-01d410f66023",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create an S3 client\n",
    "session = boto3.session.Session()\n",
    "s3_client = session.client('s3')\n",
    "sts_client = session.client('sts')\n",
    "\n",
    "# Retrieve AWS account ID and region\n",
    "account_id = sts_client.get_caller_identity()[\"Account\"]\n",
    "region = session.region_name\n",
    "# Define parameters\n",
    "bucket_name = '' # Your s3 bucket to upload the OpenAPI json file.\n",
    "file_path = 'openapi-specs/Zendesk-support-apis.yaml'\n",
    "object_key = 'Zendesk-support-apis.yaml'\n",
    "# Upload the file using put_object and read response\n",
    "try:\n",
    "    with open(file_path, 'rb') as file_data:\n",
    "        response = s3_client.put_object(Bucket=bucket_name, Key=object_key, Body=file_data)\n",
    "\n",
    "    # Construct the ARN of the uploaded object with account ID and region\n",
    "    openapi_s3_uri = f's3://{bucket_name}/{object_key}'\n",
    "    print(f'Uploaded object S3 URI: {openapi_s3_uri}')\n",
    "except Exception as e:\n",
    "    print(f'Error uploading file: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "715f371d-3118-4c7f-bb23-c90ab09e4284",
   "metadata": {},
   "source": [
    "#### Create the gateway target"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65c74344-bcea-48be-8a47-86c6d05be7c0",
   "metadata": {},
   "source": [
    "Make sure server URL in the OpenAPI file is pointing to your own endpoint URL. Gateway reads the server URL from the OpenAPI file and calls the endpoint. Before uploading it to s3, please make sure you do this change."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "629945ac-d1c3-49ae-92f7-4b0e0ef6d2c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# S3 Uri for OpenAPI spec file\n",
    "openapi_s3_target_config = {\n",
    "    \"mcp\": {\n",
    "          \"openApiSchema\": {\n",
    "              \"s3\": {\n",
    "                  \"uri\": openapi_s3_uri\n",
    "              }\n",
    "          }\n",
    "      }\n",
    "}\n",
    "\n",
    "credential_config = [\n",
    "    {\n",
    "        \"credentialProviderType\" : \"OAUTH\",\n",
    "        \"credentialProvider\": {\n",
    "            \"oauthCredentialProvider\": {\n",
    "                \"providerArn\": credentialProviderARN, \n",
    "                \"scopes\": [\"tickets:read\", \"read\", \"tickets:write\", \"write\"] \n",
    "            }\n",
    "        }\n",
    "    }\n",
    "  ]\n",
    "\n",
    "target_name=\"DemoOpenAPIGW\"\n",
    "response = gateway_client.create_gateway_target(\n",
    "    gatewayIdentifier=gatewayID,\n",
    "    name=target_name,\n",
    "    description='OpenAPI Target with S3Uri using SDK',\n",
    "    targetConfiguration=openapi_s3_target_config,\n",
    "    credentialProviderConfigurations=credential_config)\n",
    "\n",
    "# Printing the request ID and timestamp for you to report the defects. Please include them while reporting issues/defects  \n",
    "response_metadata = response['ResponseMetadata']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3ac6532-5299-4024-917d-bcd60caea6ed",
   "metadata": {},
   "source": [
    "# Calling Bedrock AgentCore Gateway from a Strands Agent\n",
    "\n",
    "The Strands agent seamlessly integrates with AWS tools through the Bedrock AgentCore Gateway, which implements the Model Context Protocol (MCP) specification. This integration enables secure, standardized communication between AI agents and AWS services.\n",
    "\n",
    "At its core, the Bedrock AgentCore Gateway serves as a protocol-compliant Gateway that exposes fundamental MCP APIs: ListTools and InvokeTools. These APIs allow any MCP-compliant client or SDK to discover and interact with available tools in a secure, standardized way. When the Strands agent needs to access AWS services, it communicates with the Gateway using these MCP-standardized endpoints.\n",
    "\n",
    "The Gateway's implementation adheres strictly to the (MCP Authorization specification)[https://modelcontextprotocol.org/specification/draft/basic/authorization], ensuring robust security and access control. This means that every tool invocation by the Strands agent goes through authorization step, maintaining security while enabling powerful functionality.\n",
    "\n",
    "For example, when the Strands agent needs to access MCP tools, it first calls ListTools to discover available tools, then uses InvokeTools to execute specific actions. The Gateway handles all the necessary security validations, protocol translations, and service interactions, making the entire process seamless and secure.\n",
    "\n",
    "This architectural approach means that any client or SDK that implements the MCP specification can interact with AWS services through the Gateway, making it a versatile and future-proof solution for AI agent integrations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3e3a7c36-d910-4651-9f1e-e2b5f605440a",
   "metadata": {},
   "outputs": [],
   "source": [
    "uv_install(\"mcp[cli]\", \"strands-agents\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "873031fe-62b5-4196-91be-500c1f87dfd4",
   "metadata": {},
   "source": [
    "# Request the access token from Okta for inbound authorization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3ed1d1d6-e84c-4286-bf25-3ad6a49723b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Requesting the access token from Okta authorizer\")\n",
    "import requests\n",
    "from requests.auth import HTTPBasicAuth\n",
    "\n",
    "# Replace with your actual values\n",
    "OKTA_DOMAIN = \"Your Okta domain URL\"\n",
    "AUTH_SERVER_ID = \"Okta app id\"\n",
    "CLIENT_ID = \"<Okta client credentials client id>\"\n",
    "CLIENT_SECRET = \"<Okta client credentials secret>\"\n",
    "\n",
    "TOKEN_URL = f\"{OKTA_DOMAIN}/oauth2/{AUTH_SERVER_ID}/v1/token\"\n",
    "\n",
    "response = requests.post(\n",
    "    TOKEN_URL,\n",
    "    auth=HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET),\n",
    "    headers={\"Content-Type\": \"application/x-www-form-urlencoded\"},\n",
    "    data={\"grant_type\": \"client_credentials\", \"scope\": \"InvokeGateway\"}\n",
    ")\n",
    "\n",
    "if response.status_code == 200:\n",
    "    token = response.json()[\"access_token\"]\n",
    "    print(\"Access Token:\", token)\n",
    "else:\n",
    "    print(\"Failed to get token:\", response.status_code, response.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6fd0379d-9576-43cb-aa9b-72b86c43b472",
   "metadata": {},
   "source": [
    "# Ask customer support agent with Zendesk support APIs using Bedrock AgentCore Gateway"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be4b39c4-7387-4ce8-b728-f2347fbdaa36",
   "metadata": {},
   "outputs": [],
   "source": [
    "from strands.models import BedrockModel\n",
    "from mcp.client.streamable_http import streamablehttp_client \n",
    "from strands.tools.mcp.mcp_client import MCPClient\n",
    "from strands import Agent\n",
    "\n",
    "def create_streamable_http_transport():\n",
    "    return streamablehttp_client(gatewayURL,headers={\"Authorization\": f\"Bearer {token}\"})\n",
    "\n",
    "client = MCPClient(create_streamable_http_transport)\n",
    "\n",
    "## The IAM group/user/ configured in ~/.aws/credentials should have access to Bedrock model\n",
    "yourmodel = BedrockModel(\n",
    "    model_id=\"us.amazon.nova-pro-v1:0\",\n",
    "    temperature=0.7,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e64794e7-4e5f-4fc5-824a-61c901e356c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from strands import Agent\n",
    "import logging\n",
    "\n",
    "\n",
    "# Configure the root strands logger. Change it to DEBUG if you are debugging the issue.\n",
    "logging.getLogger(\"strands\").setLevel(logging.INFO)\n",
    "\n",
    "# Add a handler to see the logs\n",
    "logging.basicConfig(\n",
    "    format=\"%(levelname)s | %(name)s | %(message)s\", \n",
    "    handlers=[logging.StreamHandler()]\n",
    ")\n",
    "\n",
    "with client:\n",
    "    # Call the listTools \n",
    "    tools = client.list_tools_sync()\n",
    "    # Create an Agent with the model and tools\n",
    "    agent = Agent(model=yourmodel,tools=tools) ## you can replace with any model you like\n",
    "    #print(f\"Tools loaded in the agent are {agent.tool_names}\")\n",
    "    #print(f\"Tools configuration in the agent are {agent.tool_config}\")\n",
    "    \n",
    "    # Invoke the agent with the sample prompt. This will only invoke  MCP listTools and retrieve the list of tools the LLM has access to. The below does not actually call any tool.\n",
    "    #agent(\"Hi , can you list all tools available to you\")\n",
    "    agent(\"Count the number of support tickets\")\n",
    "    # Call the MCP tool explicitly. The MCP Tool name and arguments must match with your AWS Lambda function or the OpenAPI/Smithy API\n",
    "    result = client.call_tool_sync(\n",
    "    tool_use_id=\"count-tickets-1\", # You can replace this with unique identifier. \n",
    "    name=\"DemoOpenAPIGW___CountTickets\", # This is the tool name based on AWS Lambda target types. This will change based on the target name\n",
    "    )\n",
    "    #Print the MCP Tool response\n",
    "    print(f\"Tool Call result: {result['content'][0]['text']}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa8dd91d-15a6-45b2-b539-bb3eaf0e0e08",
   "metadata": {},
   "source": [
    "# Clean up\n",
    "Additional resources are also created like IAM role, IAM Policies, Credentials provider, AWS Lambda functions, Cognito user pools, s3 buckets that you might need to manually delete as part of the clean up. This depends on the example you run."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a22f158d-83b6-4d65-892f-9a208af3d742",
   "metadata": {},
   "source": [
    "## Delete the gateway (Optional)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c3edc5b6-e5f0-47b9-994f-378415bad50d",
   "metadata": {},
   "outputs": [],
   "source": [
    "import utils\n",
    "utils.delete_gateway(gateway_client,gatewayID)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
